package org.jprobe.classpath;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
 * classpath reader implementation
 * @author Hussain Al-Mutawa
 * @version 1.0
 * @since 1.0
 */
public abstract class AbstractClassPathReader implements ClassPathReaderInterface {
	/** tracks how many instances of this object are in the memory **/
	private static int instancesCounter=0;
	/** the build directory name */
	public transient static String buildDirectory="bin";
	/** the java home path, an empty string is added to prevent throwing an exception in case System.getProperty(java.home) fails **/
	public transient static String JAVA_HOME = (System.getProperty("java.home")+"").replaceAll("\\\\", "/");
	/** the path to the java runtime library **/
	public transient static String JAVA_RUNTIME_LIBRARY = JAVA_HOME+"/lib/rt.jar";

	/** the classpath container **/
	private transient final AbstractClassPath classpath;
	/** the full path of the build directory **/
	private transient final String buildDirectoryFullPath;
	/** the universal classloader used to modify the classpath **/
	private transient static URLClassLoader classLoader = (URLClassLoader) Thread.currentThread().getContextClassLoader();

	/** prevent multiple copies of the same object **/
	protected AbstractClassPathReader(){
		this(JAVA_RUNTIME_LIBRARY);
	}
	/** instantiate the classpath reader and assigns the path to the javaruntime library path **/
	public AbstractClassPathReader(final String javaRuntimeLibraryPath){
		super();
		if(instancesCounter>0){
			throw new UnsupportedOperationException("This object has been already initialized, use one copy only");
		}
		classpath=new AbstractClassPath(){
			@Override
			public String getJavaRuntimeLibrary() {
				return javaRuntimeLibraryPath;
			}
			@Override
			public ClassLoader getClassloader() {
				return classLoader;
			}
		};

		try {
			final File buildPath = new File(getProjectDirectory()+'/'+getBuildDirectory());
			buildDirectoryFullPath = buildPath.getCanonicalPath().replaceAll("\\\\", "/");
			addClassPathUrlToSystemClassLoader(buildPath);
			loadJavaRuntimeLibraryPackages();
			addClassPathUrlToSystemClassLoader(new File(getProjectDirectory()+"/bin"));
		} catch (Exception exp) {
			throw new UnsupportedOperationException(exp);
		}
		instancesCounter++;
	}
	/**
	 * gets the path to the java runtime library
	 * @return physical path in the file system for java runtime library
	 */
	public abstract String getProjectDirectory();
	/**
	 * gets the name of the build directory of the project
	 * @return build directory name
	 */
	public abstract String getBuildDirectory();

	@Override
	public Set<String> loadPackages(final File directory) throws IOException{
		return loadPackages(directory, directory);
	}
	/**
	 * gets the packages in a given directory
	 * @param directory the project build directory
	 * @param subdirectory the directory holding classes
	 * @return set of parent classes
	 * @throws IOException if the path is invalid
	 * @return set of packages read from the classpath
	 */
	protected Set<String> loadPackages(final File directory,final File subdirectory) throws IOException{
		File[]files=subdirectory.listFiles(new FileFilter() {
			@Override
			public boolean accept(File pFile) {
				return pFile.getName().toLowerCase().endsWith(".class");
			}
		});
		final Set<String> packages = new HashSet<String>();
		final String projPath = directory.getCanonicalPath();//(getProjectDirectory()+'/'+getBuildDirectory()).replaceAll("\\\\", "/");
		for(final File file : files){
			final String path = directory.getCanonicalPath();
			final String name = file.getCanonicalPath().replace(projPath, "").replaceAll("\\\\", "/");
			if(!isAnonymous(name)){
				//addClassToCLASSPATH(new ClassEntry(path,name));
				final ClassEntry entry = new ClassEntry(path,name);
				packages.add(entry.getPackageName());
				addClassToCLASSPATH(entry);
			}

		}
		files=subdirectory.listFiles(new FileFilter() {
			@Override
			public boolean accept(final File pFile) {
				return pFile.isDirectory();
			}
		});
		for(final File file:files){
			packages.addAll(loadPackages(directory,file));
		}
		return packages;
	}
	/**
	 * check if the class is anonymous or not,
	 * the assumption is that
	 * @param name
	 * @return
	 */
	private boolean isAnonymous(final String name) {
		return name.matches(".*\\$\\d+\\.class$");
	}
	/**
	 * gets the full path of the build directory
	 * @return the build path directory
	 */
	protected String getBuildDirectoryFullPath() {
		return buildDirectoryFullPath;
	}

	@Override
	public void loadJavaRuntimeLibraryPackages() throws IOException {
		//loadPackages(new JarFile(JAVA_RUNTIME_LIBRARY));
		getClassPath().addEntry(JAVA_RUNTIME_LIBRARY);
		final char NL = '\n';
		final char CL = '\r';
		final InputStream is = getClass().getResourceAsStream("java.libraries");

		String line="";
		for(int c=is.read();c!=-1;c=is.read()){
			if(c==NL || c==CL){
				if(!"".equals(line)){
					addClassToCLASSPATH(new ClassEntry(JAVA_RUNTIME_LIBRARY,line));
				}
				line="";
			}else{
				line+=(char)c;
			}
		}
		is.close();
	}

	@Override
	public Set<String> loadPackages(final JarFile jar) throws IOException{
		final Set<String>packages=new HashSet<String>();
		final Enumeration<JarEntry>jarEntries=jar.entries();
		while(jarEntries.hasMoreElements()){
			final JarEntry jarEntry=jarEntries.nextElement();
			final String name = jarEntry.getName();
			if(name.endsWith(".class")){
				final ClassEntry entry = new ClassEntry(jar.getName(),name);
				packages.add(entry.getPackageName());
				addClassToCLASSPATH(entry);
			}
		}
		jar.close();
		return packages;
	}

	@Override
	public void addClassToCLASSPATH(final ClassEntryInterface classEntry) {
		try{
			final PackageInterface pkg;
			final String packageName = classEntry.getPackageName();
			if(!classpath.getPackages().contains(packageName)){
				pkg = new Package(classEntry.getClasspath(),packageName);
				classpath.addPackage(pkg);
			}else{
				pkg = classpath.getPackage(packageName);
			}
			final String className = classEntry.getClassName();
			pkg.addClass(className);


		}catch(Exception exp){
			throw new RuntimeException("Class ["+classEntry+"] threw an exception - "+exp.getMessage(),exp);
		}
	}
	@Override
	public ClassPathInterface getClassPath(){
		return this.classpath;
	}

	@Override
	public String[] addClassPathUrlToSystemClassLoader(final String path) throws Exception{
		return addClassPathUrlToSystemClassLoader(new File(path));
	}

	@Override
	public String[] addClassPathUrlToSystemClassLoader(final File file) throws Exception {

		//reference to the classloader class
		final Class<?> clazz = URLClassLoader.class;

		// Use reflection
		final Method method = clazz.getDeclaredMethod("addURL", new Class<?>[] { URL.class });
		method.setAccessible(true);
		final URL url = file.toURI().toURL();

		method.invoke(classLoader, new Object[] { url });

		//update java runtime classpath
		String cp = System.getProperty("java.class.path");
		//System.out.println(cp);
        if (cp != null) {
            cp += File.pathSeparatorChar + file.getCanonicalPath();
        } else {
            cp = file.toURI().getPath();
        }
        System.setProperty("java.class.path", cp);
        Set<String> packages;// = new HashSet<String>();
		try{
			packages=loadPackages(new JarFile(file));
		}catch(IOException ioe){
			packages=loadPackages(file);
		}
		getClassPath().addEntry(file.getCanonicalPath());

		return packages.toArray(new String[packages.size()]);
	}
	/** gets the url classloader used in this context, @return url class loader **/
	public static URLClassLoader getClassloader() {
		return classLoader;
	}

	@Override
	public void removeClassPathUrlFromSystemClassLoader(final String path) throws Exception{
		final File file = new File(path);
		final List<URL> urls = new ArrayList<URL>(Arrays.asList(classLoader.getURLs()));
		classLoader = null;
		System.gc();
		//System.out.println(urls.size()+"{{{");
		String cp = System.getProperty("java.class.path");
		//System.err.println(cp);
        if (cp != null) {
            cp = cp.replace(file.getCanonicalPath(),"");
            urls.remove(getURL(urls,file.toURI().toURL()));
        }else{
        	cp = Arrays.toString(urls.toArray()).replace("[", "").replace("]", "").replace("file:", "").replace(',', File.pathSeparatorChar).replaceAll("\\;\\;", ";");
        }
        //System.out.println(urls.size()+"]]]");
        System.setProperty("java.class.path", cp.replaceAll("\\;\\;", ";"));
        classLoader = URLClassLoader.newInstance(urls.toArray(new URL[urls.size()]));
        getClassPath().removeClassPathEntry(file.getCanonicalPath());
	}

	private URL getURL(final List<URL>urls,final URL url){
		for(final URL u:urls){
			if(u.toString().equals(url.toString())){
				return u;
			}
		}
		return null;
	}
}
